package com.ho1ho.springboot.framework.core;

import javax.annotation.Resource;
import javax.sql.DataSource;

import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;

// http://docs.spring.io/spring-security/site/docs/3.2.0.RC2/reference/htmlsingle/#jc
//@Configuration
//@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
// @PropertySource("classpath:application.properties")
public abstract class BaseWebSecurityConfiguration extends WebSecurityConfigurerAdapter {

	public static final String PARAMETER_USER_NAME = "username";

	public static final String REMEMBER_ME_COOKIE_NAME = "remember-me";

	public static final String REMEMBER_ME_KEY = "remember-me-key-a-rAndOm-kEy-a5jEd$3jd^fBj*xh3}hIjA";
	
	public static final String REMEMBER_ME_PARAM = "remember-me";
	
	public static final String ACCESS_DENIED_PATH = "/access_denied";
	
	public static final String ERROR_PARAMETER_INVALID = "invalid";
	
	public static final String ERROR_PARAMETER_EXPIRED = "expired";

	@Resource
	protected UserDetailsService userDetailsServiceImpl;

	@Resource
	private AuthenticationProvider authenticationProviderImpl;

	@Resource
	protected BCryptPasswordEncoder bCryptPasswordEncoder;

	@Resource
	protected Environment environment;
	
	@Resource
	protected DataSource dataSource;

	public abstract AuthenticationSuccessHandler getAuthenticationSuccessHandler();

	public abstract AuthenticationFailureHandler getAuthenticationFailureHandler();
	
	@Bean
	public BCryptPasswordEncoder bCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}

	// https://stackoverflow.com/a/24957247
	// https://stackoverflow.com/a/27175666
	// You must set this method and using [static], otherwise, you can not login again if you logout.
	// If you set maximumSessions(1), you need also add following method.
	// Add following method, when you logout, the previous session will be invalid.
	@Bean
	public static ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
		return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
	}

	@Bean
	public SessionRegistry sessionRegistry() {
		return new SessionRegistryImpl();
	}
	
	// https://stackoverflow.com/a/30344608
//	@Bean
////	@Primary
//	@ConfigurationProperties(prefix = "spring.datasource")
//	public DataSource dataSource() {
//		return DataSourceBuilder.create().build();
//	}

	// https://docs.spring.io/autorepo/docs/spring-security/5.0.4.RELEASE/reference/htmlsingle/#remember-me
	@Bean
	public PersistentTokenRepository persistentTokenRepository() {
		JdbcTokenRepositoryImpl tokenRepositoryImpl = new JdbcTokenRepositoryImpl();
		tokenRepositoryImpl.setDataSource(dataSource);
		return tokenRepositoryImpl;
	}

	// https://docs.spring.io/autorepo/docs/spring-security/5.0.4.RELEASE/reference/htmlsingle/#remember-me
	// https://stackoverflow.com/a/32372834
//	@Bean
//	public PersistentTokenBasedRememberMeServices getPersistentTokenBasedRememberMeServices() {
//		PersistentTokenBasedRememberMeServices tokenBasedservice = new PersistentTokenBasedRememberMeServices(
//				REMEMBER_ME_KEY, userDetailsServiceImpl, persistentTokenRepository());
//		return tokenBasedservice;
//	}

//	@Bean
//	public AuthenticationTrustResolver getAuthenticationTrustResolver() {
//		return new AuthenticationTrustResolverImpl();
//	}

	@Override
    public void configure(WebSecurity web) throws Exception {
        web
        	.ignoring()
        		.antMatchers(
        					"/webjars/**",
        					"/public/**",
        					"/static/**",
        					"/imageviewer/**",
        					"/favicon.**",
        					"/img/**",
                        "/css/**",
                        "/js/**",
                        "/fonts/**");
    }
	
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
	            .antMatchers("/api/**").permitAll()
	            .antMatchers("/debug/**").permitAll()
//	            .antMatchers("/resources/**").permitAll()
//	            .antMatchers("**").permitAll()
//	            .antMatchers("/admin/**").hasRole("ADMIN")
//	            .antMatchers("/db/**").access("hasRole('ROLE_ADMIN') and hasRole('ROLE_DBA')")
                .antMatchers(HttpMethod.GET, "/", "/login", ACCESS_DENIED_PATH).permitAll()
                .antMatchers("/registration").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
	            .loginProcessingUrl("/login") // Default value. Use this url to do login process
                .loginPage("/login") // Login form page url
                // Default process by spring is redirecting to "/login?error" using SimpleUrlAuthenticationFailureHandler.
                // If use defaultSuccessUrl() method, successHandler() will be ignored.
                .successHandler(getAuthenticationSuccessHandler())
                .failureHandler(getAuthenticationFailureHandler())
                // If use defaultSuccessUrl() method, successHandler() will be ignored.
                // Use defaultSuccessUrl method, it has a major function that
                // if you access authority resource but redirect to login page,
                // then after login, you will be back to you last access url.
//                .defaultSuccessUrl("/dashboard")
//                .failureUrl("/login?error") // Default value by Spring
                .usernameParameter(PARAMETER_USER_NAME).passwordParameter("password")
                // http://docs.spring.io/spring-security/site/docs/current/guides/html5/form-javaconfig.html
                // The method formLogin().permitAll() statement instructs Spring Security 
                // to allow any access to any URL (i.e. /login and /login?error) associated to formLogin().
                .permitAll()
                .and()
            // https://stackoverflow.com/a/32926962
    		    // https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html
            // https://docs.spring.io/autorepo/docs/spring-security/5.0.4.RELEASE/reference/htmlsingle/#csrf
            .csrf()
//                .disable()
                .and()
            .exceptionHandling().accessDeniedPage(ACCESS_DENIED_PATH)
                .and()
            .logout()
	            .clearAuthentication(true)
	            .invalidateHttpSession(true)
//		        .logoutRequestMatcher(new AntPathRequestMatcher("/logout**"))
	            .logoutSuccessUrl("/login?logout") // Used spring default value
	            .deleteCookies(new String[] { REMEMBER_ME_COOKIE_NAME, "JSESSIONID" })
	            .permitAll()
	            .and()
	            // https://qiita.com/opengl-8080/items/7c34053c74448d39e8f5#key-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6
	            // https://docs.spring.io/spring-security/site/docs/current/reference/html/remember-me.html
	            // https://docs.spring.io/autorepo/docs/spring-security/5.0.4.RELEASE/reference/htmlsingle/#remember-me
            .rememberMe()
//                .useSecureCookie(true)
	            .rememberMeCookieName(REMEMBER_ME_COOKIE_NAME)
//		            .rememberMeParameter(REMEMBER_ME_PARAM)
		        .key(REMEMBER_ME_KEY)
//	            .rememberMeServices(getPersistentTokenBasedRememberMeServices())
		        .userDetailsService(userDetailsServiceImpl)
	            .tokenRepository(persistentTokenRepository())
	            .tokenValiditySeconds(Const.TOKEN_VALIDITY_SECONDS)
	            .and()
	        // https://docs.spring.io/spring-security/site/docs/current/reference/html/ns-config.html#ns-session-mgmt
            	.sessionManagement()
	            	// https://qiita.com/opengl-8080/items/ad9159910501d1989876
	            	// If you set this line below, the session id will not change after login.
            		// https://docs.spring.io/spring-security/site/docs/current/reference/html/ns-config.html#ns-session-fixation
	            	.sessionFixation().newSession() // or .changeSessionId() // .newSession()
	            	// If the same user tries to login in two or more browsers, it will cause 
	            	// [Maximum sessions of 1 for this principal exceeded] exception.
	        		.maximumSessions(1)
	        		// https://stackoverflow.com/a/36970658
	        		// ConcurrentSessionFilter will redirect to expiredUrl, 
	        		// if the valid session ID is marked as expired in SessionRegistry.
	        		// https://docs.spring.io/autorepo/docs/spring-security/5.0.4.RELEASE/reference/htmlsingle/#nsa-concurrency-control-attributes
	        		// But I don't know how to reproduce this error.
	        		.expiredUrl("/login?" + ERROR_PARAMETER_EXPIRED)
	        		// 设置该参数为 false 时，如果同一账号已经登录过（需要选择 Remember Me），若此时使用其它浏览器再次登录时（不必须选择 Remember Me），
	        		// 会触发 expired 处理。即第二次登录会覆盖第一次登录。
	        		// 详细处理参见 com.ho1ho.teacherevaluation.web.auth.AuthController.login(HttpServletRequest) 方法。
	        		// 设置该参数为 true 时，如果同一账号已经登录过，若此时使用其它浏览器再次登录时，会提示“用户已经登录”。即第二次无法登录。
	        		.maxSessionsPreventsLogin(false)
//	        		 .expiredSessionStrategy(new CustomSessionInformationExpiredStrategy())
	        		.sessionRegistry(sessionRegistry())
        			.and()
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            // https://stackoverflow.com/a/36970658
            // The following url will be used when session is timeout or wrong id.
            // https://docs.spring.io/autorepo/docs/spring-security/5.0.4.RELEASE/reference/htmlsingle/#nsa-session-management-attributes
            .invalidSessionUrl("/login?" + ERROR_PARAMETER_INVALID);
    }
	
//	public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
//	    @Override
//	    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
//	        DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
//	        redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "/login?" + ERROR_PARAMETER_EXPIRED);
//	        
////	        HttpServletResponse response = event.getResponse();
////	        response.getWriter().print("This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).");
////	        response.flushBuffer();
//	    }
//	}
	
	@Resource
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		// In docker mode only.
		// Create a user in memory.
		if (environment.acceptsProfiles(Const.CURRENT_RUNNING_PROFILE_DOCKER)) {
			auth
				.inMemoryAuthentication()
					.withUser("y@ho1ho.com")
						.password("12345678")
						.roles("USER")
						.and()
					.withUser("admin@ho1ho.com")
						.password("12345678")
						.roles("USER", "ADMIN");
		} else {
			/*
			auth
				.userDetailsService(userDetailsServiceImpl).passwordEncoder(bCryptPasswordEncoder).and()
				// If authentication successful, do not process userDetailsService in WebSecurityConfig class.
				// https://aykutakin.wordpress.com/2013/07/08/spring-security-spring-custom-authentication-provider/
				// http://www.jianshu.com/p/42745873ebfb
				// Default value: DaoAuthenticationProvider
				.authenticationProvider(authenticationProviderImpl);
			*/
			auth
				.authenticationProvider(authenticationProviderImpl)
				.userDetailsService(userDetailsServiceImpl)
				.passwordEncoder(bCryptPasswordEncoder);
		}
	}

    // http://www.jianshu.com/p/59787a123b05
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}